#!/usr/bin/env python

"""
__author__ = Zdenek Maxa

fdtcp project (aimed at integration of FDT and phedex)

https://twiki.cern.ch/twiki/bin/view/Main/PhEDExFDTIntegration
http://code.google.com/p/phedex-fdt/


this program is client for remote fdtd data transfer services,
communication is done via PYRO (Python Remote Objects)

Destination Python version is 2.4

Command line options format should follow (to certain extent) srmcp interface:
srmcp help says:

Usage: srmcp [command line options] source(s) destination

 or  : srmcp [command line options] -copyjobfile=<file>
       either source(s) or destination or both should be (an) srm url
       default options can be set in configuration file 
       or overridden by the command line options
       
       
Report file (--report) is required by PhEDEx. PhEDEx itself makes its own checks
    whether or not the files were successfully transferred, but these report flags
    are used for debugging. srmcp defines these return codes:
     0 - success
     1 - general error
     2 - file exists, can not overwrite
     3 - user permission error

    fdtcp implements only 0 or 1. And also doesn't support partial transfers - so
        whole transfer is either considered successful or at error.
        If partial transfers are to be reported properly, the report file needs
        to be generated by FDT Java since fdtcp can't get such information from
        FDT Java - the command either successfully finishes or fails - and whole
        transfer which is typically a bunch of files is handled by a single
        FDT client, server pair. Consideration of these partial transfers are
        certainly not a priority at this stage.
    
"""

"""
TODO

o revise concept to combine result status values (Result.status) and exceptions
    raising - should (probably) use just one concept ...
        
o if copyjob file is very often a mix of various source hosts, destination hosts
    then, Transfer.performTransfer() methods should run concurrently so that
    independent transfers don't wait on one another
    
"""



import os
import sys
sys.path.append("/usr/lib/python2.4/site-packages")
import copy
import time 
import signal
import logging
import socket
import re
import datetime
try:
    # force loading fdtcplib.__init__.py (see comment in this file related
    # to PYRO_STORAGE env. variable setting) - must be before first PYRO import
    import fdtcplib
    from ApMon import apmon
    import Pyro
    import Pyro.core
    from Pyro.errors import PyroError
    from Pyro.errors import ProtocolError
    from Pyro.errors import URIError
    from Pyro.errors import ConnectionClosedError
    from fdtcplib.utils.Executor import ExecutorException
    from fdtcplib.utils.Config import Config
    from fdtcplib.utils.Logger import Logger
    from fdtcplib.common.actions import TestAction, ReceivingServerAction, SendingClientAction
    from fdtcplib.common.actions import CleanupProcessesAction
    from fdtcplib.common.actions import AuthServiceAction
    from fdtcplib.common.actions import AuthClientAction
    from fdtcplib.common.actions import Result
    from fdtcplib.common.TransferFile import TransferFile
    from fdtcplib.common.errors import FDTDException, TimeoutException, FDTCopyException
    from fdtcplib.utils.Config import ConfigurationException
except ImportError, ex:
    print "Can't import dependency modules: %s" % ex
    sys.exit(1)

 

    
class FDTCopy(object):
    """PYRO client / proxy - all interactions with remote FDTD PYRO service
       happen via this object.
    """
    
    def __init__(self, uri, logger):
        self.logger = logger
        self.uri = uri
        self.logger.debug("%s creating PYRO proxy for URI: '%s'" %
                         (self.__class__.__name__, self.uri))
        try:
            Pyro.core.initClient()
            self.proxy = Pyro.core.getProxyForURI(self.uri)
        except (AttributeError, URIError), ex:
            m = "Incorrect URI '%s', could not create PYRO proxy for FDTD service." % self.uri
            raise FDTCopyException(m)
        
        # set timeout - if there is a firewall, usually hangs for long
        # this PYRO solution does not work - still hangs for 3 minutes ...
        # -> solution by signal / SIGALARM
        #self.proxy._setTimeout(num_of_seconds)
        signal.signal(signal.SIGALRM, self._raiseTimeoutException)
        
            
        
    def _raiseTimeoutException(self, *args):
        raise TimeoutException("Timeout exception.")
    
        
         
    def call(self, action):
        self.logger.debug("Calling '%s' request: %s ..." % (self.uri, action))
        try:
            try:
                if action.timeout:
                    signal.alarm(action.timeout) # raise alarm in timeout seconds
                result = self.proxy.service(action)
            finally:
                signal.alarm(0) # disable alarm
        except TimeoutException, ex:
            m = "Call to remote PYRO FDTD service timed-out (firewall in between?)."
            raise FDTCopyException(m)
        # ProtocolError first, before PyroError
        except ProtocolError, ex:
            m = "ProtocolError during remote PYRO call, reason: %s" % ex
            self.logger.error("PYRO call traceback: %s" % ''.join(Pyro.util.getPyroTraceback(ex)))
            raise FDTCopyException(m)        
        except PyroError, ex:
            self.logger.error("PYRO call traceback: %s" % ''.join(Pyro.util.getPyroTraceback(ex)))            
            m = "PyroError during remote PYRO call, reason: %s" % ex
            raise FDTCopyException(m)
        except ConnectionClosedError, ex:
            m = "Connection with remote PYRO service lost, try again."
            raise FDTCopyException(m)
        else:
            #self.logger.debug("Call to remote PYRO service successful.")
            return result
        
        
        
    def perform(self, action):
        result = self.call(action)
        if result.status == 0:
            self.logger.info("Success, response: %s" % result)
            return result
        else:
            m = ("Error occurred on host %s id: %s, reason: %s\n"
                 "remote log:\n%s\n" % (result.host, result.id, result.msg, result.log))
            raise FDTCopyException(m)     
            
               
   

class Transfer(object):
    """Transfer of one or more files from host A to host B.
       Implements group transfers.
       Grid authentication chain, i.e. starting AuthService at remote A, B
           hosts and getting authenticated with both via local AuthClient
           is a series of actions associated with an instance of Transfer
       TODO
       consider implementing iterator protocol
    """
    def __init__(self, conf, apMon, logger):
        self.id = None # transfer process ID
        self.hostSrc = None # host name
        self.hostDest = None # host name
        self.portSrc = None # port on which fdtd at source site runs
        self.portDest = None # port on which fdtd at destination site runs
        self.sender = None  # FDTCopy instance to the remote sending FDT client
        self.receiver = None # FDTCopy instances to the remote receiving FDT server
        self.files = [] # list of TransferFile - will be translated into fileList
        self.log = None # transfer log as returned from remote FDTD service
        self.result = None # result of transfer on hosts (or group transfers) level
        self.toCleanup = [] # URI of remote parties to send CleanupProcessAction to
        self.conf = conf
        self.apMon = apMon # instance for ApMon, MonALISA monitoring
        self.logger = logger
        
        
        
    def __str__(self):
        numFiles = len(self.files)
        if numFiles > 1:
            m = "%s -> %s (%s files)" % (self.hostSrc, self.hostDest, numFiles)
        else:
            m = ("%s:%s -> %s:%s" % (self.hostSrc, self.files[0].fileSrc,
                                     self.hostDest, self.files[0].fileDest))
        return m
           
           
    
    def setUp(self, hostSrc, portSrc, hostDest, portDest, trFile):
        self.files.append(trFile)
        self.hostSrc, self.hostDest = hostSrc, hostDest
        self.portSrc, self.portDest = portSrc, portDest
        
        uriSrc = "PYROLOC://" + hostSrc + ":" + portSrc + "/FDTDService"
        uriDest = "PYROLOC://" + hostDest + ":" + portDest + "/FDTDService"
        
        # assuming FDT Java client is sender (at respective remote FDTD service)
        # assuming FDT Java server is receiver (at respective remote FDTD service)
        # create local PYRO proxies for remote FDTD services
        try:
             self.sender = FDTCopy(uriSrc, self.logger)
             self.receiver = FDTCopy(uriDest, self.logger)
        except FDTCopyException, ex:
            m = "Transfer %s failed, reason: %s" % (self, ex)
            self.logger.error(m)
            self.log = m
            self.result = 1
            
            
            
    def addFile(self, trFile):
        self.files.append(trFile)
        
        
        
    def _runAuthChain(self):
        # 1) contact remote parties in order to obtain port on which
        # AuthService runs, then run AuthClient locally to perform
        # authentication first with the destination, then with source site
        authServiceAction = AuthServiceAction(self.id)

        # inconsistent design - catching exceptions / checking result status?
        result = self.receiver.perform(authServiceAction)
        
        authServicePortReceiver = result.serverPort
        self.logger.info("Remote AuthService (receiver): %s:%s" %
                         (result.host, authServicePortReceiver))
        
        result = self.sender.perform(authServiceAction)
        
        authServicePortSender = result.serverPort
        self.logger.info("Remote AuthService (sender): %s:%s" %
                         (result.host, authServicePortSender))
                
        # 2) run local AuthClient and authenticate with A, then B
        # set options so that AuthClient know which host:port to contact
        options = dict(host = self.hostDest, port = authServicePortReceiver)        
        authClientAction = AuthClientAction(self.id, options)
        
        # execute locally AuthClient and authenticate with destination remote site
        # (the one which will receive files)
        result, remoteGridUserDest = authClientAction.execute(conf = self.conf, logger = self.logger)
        
        self.logger.debug("Authentication client result: %s, Grid user name "
                          "at destination: %s" % (result, remoteGridUserDest))
        
        # inconsistent design - catching exceptions / checking result status?
        if result.status != 0:
            raise FDTCopyException("Authentication failed, reason: %s" % result.msg)


        # if no exception was raised here, first local-"receiver site"
        # authentication was successful

        # run local AuthClient - source remote site (the one which will send files)
        # set options so that AuthClient know which host:port to contact
        options = dict(host = self.hostSrc, port = authServicePortSender)        
        authClientAction = AuthClientAction(self.id, options)
        
        # execute locally AuthClient
        result, remoteGridUserSrc = authClientAction.execute(conf = self.conf, logger = self.logger)
        
        self.logger.debug("Authentication client result: %s, Grid user name "
                          "at source: %s" % (result, remoteGridUserSrc))
        
        # inconsistent design - catching exceptions / checking result status?
        if result.status != 0:
            raise FDTCopyException("Authentication failed, reason: %s" % result.msg)
        
        # if no exception was raised, authentication chain was successful
        
        # no need to perform clean up - AuthService processes are running only
        # in single instances at remote sites and taken care of by the daemon process
        
        self.logger.info("Authentication chain was successful.")
        
        return remoteGridUserSrc, remoteGridUserDest
                
        
        
    def performTransfer(self):
        self.logger.info("Starting transfer  %s" % self)    
        
        if self.result != None:
            self.logger.warn("Skipping transfer %s (already failed)." % self)
            return
        
        # timeout is used only on TestActions (testing mutual connection)
        # id of the initial action (this test (TestAction)) is later used in all
        # subsequent actions related to this particular transfer
        timeout = int(self.conf.get("timeout"))
        testAction = TestAction(timeout = timeout)
        self.id = testAction.id
        
        self.logger.info("Testing remote parties availability %s ..." % testAction)
    
        self.receiver.perform(testAction)
        self.sender.perform(testAction)
        
        # remote services are available, authenticate with both
        # remoteGridUserSrc - user's name at the source site
        # remoteGridUserDest - user's name at the destination site
        authStartTime = datetime.datetime.now()
        remoteGridUserSrc, remoteGridUserDest = self._runAuthChain()
        authEndTime = datetime.datetime.now()
        authTime = (authEndTime - authStartTime).seconds
        par = dict(id = self.id, authentication = authTime)
        self.logger.debug("Authentication lasted: %s [s], logging with ApMon." % authTime)
        self.apMon.sendParameters("fdtcp", None, par)
        
        
        # find out client IP address, server will need it (-f <allowedIPsList>) when starting
        clientIP = socket.gethostbyname(self.hostSrc)
        self.logger.debug("Client IP address is: '%s'" % clientIP)
        
        # start receiving server first, information on its port will need the client
        
        options = dict(gridUserDest = remoteGridUserDest,
                       clientIP = clientIP)
        recvServerAction = ReceivingServerAction(self.id, options)   
        result = self.receiver.perform(recvServerAction)
        serverFDTPort = result.serverPort
        self.logger.info("Remote FDT server: %s:%s" % (result.host, serverFDTPort))
                
        # no exception was raised, remote process must be running, register for
        # clean up (remote process may however terminate normally, then shall be
        # removed from the container of processes at the remote party)
        self.toCleanup.append(self.receiver.uri)
    
        # start sending FDT client which initiates the transfer process
        options = dict(port = serverFDTPort, hostDest = self.hostDest,
                              transferFiles = self.files,
                              gridUserSrc = remoteGridUserSrc)   
        sndClientAction = SendingClientAction(self.id, options)
        
        # TODO ... can't be propagating exceptions here ... they have to be handled
        # here to process results (and do report file)
                
        # calling the client is synchronous action - waiting until it finishes
        # if the client hangs without progress - may want to kill such remote
        # process, thus register the remote URI into clean up local container
        # if something goes wrong, this remote URI later receives CleanupAction
        # to kill its running processes
        self.toCleanup.append(self.sender.uri)
    
        # this command will stop the execution flow, for very very long if
        # the transfer data is bulky - status monitoring via MonALISA
        # using the transfer id
        result = self.sender.perform(sndClientAction)
        
        self.logger.info("Transfer result: %s, logs (FDT sending client):\n%s" % (result, result.log))
        
        self.result = result.status
        # don't put the log there for now
        # self.log = result.log
        
        # TODO
        # it may be to retrieve also server logs (though FDT server errors
        # should be propagated to FDT client), it may be easy to get from the
        # cleanup action which gets the logs ...
        
        # clean up remote processes
        # the server - FDT Java server is run with -S - it gets automatically shut
        #    once the transfer is overe
        # the client - if everything was all right (client not hanging), it
        #    has already successfully finished (and the key of the process
        #    has been removed from the container with the client)
        self.performCleanup()
        
        
        
    def performCleanup(self):
        if len(self.toCleanup) < 1:
            self.logger.info("Cleanup action - nothing to clean up, transfer: %s" % self)
            return
    
        cleanupStartTime = datetime.datetime.now()
        
        self.logger.info("Cleanup action, transfer: %s - terminate remote processes ..." % self)
        self.logger.debug("%s items in the clean up container." % len(self.toCleanup))
        timeout = int(self.conf.get("timeout"))
        cl = CleanupProcessesAction(self.id, timeout = timeout)
        
        # iterate the container from the tail, the last one added was client and
        # client should have finished by this point anyway ...
        self.toCleanup.reverse()
        # have to have another copy of the container to iterate
        # over, the original will be shrunk
        tmpToCleanup = copy.deepcopy(self.toCleanup)
        
        for uri in tmpToCleanup:
            # have to try to get another PYRO proxy, should the previous one be blocked
            fdtCopy = FDTCopy(uri, self.logger)
            try:
                fdtCopy.perform(cl)
            except FDTCopyException, ex:
                self.logger.error("During clean up: %s" % ex)
            # TODO
            # have to be specific about exceptions here, shall exhaust the whole list though
            # likely FDTDException may happen here and is translated into FDTCopyException
            # above
            except Exception, ex:
                self.logger.error("Unspecified exception during clean up: %s" % ex)

            fdtCopy.proxy._release()    
            self.toCleanup.pop() # reverse, then pop the last item, iterating from the end ...

        cleanupEndTime = datetime.datetime.now()
        cleanupTime = (cleanupEndTime -cleanupStartTime).seconds
        par = dict(id = self.id, cleanup = cleanupTime)
        self.logger.debug("Clean up lasted: %s [s], logging with ApMon." % cleanupTime)
        self.apMon.sendParameters("fdtcp", None, par)
        
                                 

    
class Transfers(object):
    """Wrapper for all transfers, does copyjobfile processing if specified.
       TODO:
       consider implementing iterator (more elegant?) protocol (instead transfers.transfers) 
    """
    def __init__(self, conf, apMon, logger):
        # dict of Transfer class instances
        # key is hostPortSrc+hostPortDest - processing for copyjobfile - transfers grouping
        self.transfers = {}
        
        if not conf.get("copyjobfile"):
            # consider no copyjobfile scenario - must be a single file            
            self._setTransfer(conf.get("urlSrc"), conf.get("urlDest"), apMon, conf, logger)
        else:
            self._processCopyJobFile(conf, apMon, logger)
            
            
            
    def _setTransfer(self, urlSrc, urlDest, apMon, conf, logger):
        """Initialise instance of Transfer class and set up properties.
           URL form: fdt://gridftp01.ultralight.org:8443//mnt/hadoop/path/to/file
                     fdt://.*:[0-9]+/?.*
           Aggregates the same source, destination hosts into group transfer.
           Same source, destination value is defined by hostSrc:portSrc-hostDest:postDest
        """
        pattern = re.compile("fdt://(?P<host>.*):(?P<port>[0-9]+)/?(?P<file>/.*)")
        
        if not pattern.match(urlSrc):
            raise FDTCopyException("Wrong format of '%s'" % urlSrc)
        if not pattern.match(urlDest):
            raise FDTCopyException("Wrong format of '%s'" % urlDest)
        
        m = pattern.search(urlSrc)
        hostSrc, portSrc, fileSrc = m.group("host"), m.group("port"), m.group("file")
        
        m = pattern.search(urlDest)
        hostDest, portDest, fileDest = m.group("host"), m.group("port"), m.group("file")

        # check if such transfer exists, if so, aggregate, if not, create it
        key = "%s:%s-%s:%s" % (hostSrc, portSrc, hostDest, portDest)
        try:
            transfer = self.transfers[key]
            transfer.addFile(TransferFile(fileSrc, fileDest))
        except KeyError:
            transfer = Transfer(conf, apMon, logger)
            trFile = TransferFile(fileSrc, fileDest)
            transfer.setUp(hostSrc, portSrc, hostDest, portDest, trFile)
            self.transfers[key] = transfer
                            
            
            
    def _processCopyJobFile(self, conf, apMon, logger):
        """Process copyjobfile - list of pairs urlSrc urlDest (resp. FROM_PFN TO_PFN).
        """
        fileName = conf.get("copyjobfile")
        try:
            lines = open(fileName, 'r').readlines()
        except IOError, ex:
            raise FDTCopyException("Can't read '%s', reason: %s" % (fileName, ex))
        
        try:
            for index, line in enumerate(lines):
                line = line.strip()
                urls = line.split()
                urlSrc, urlDest = urls[0], urls[1]
                self._setTransfer(urlSrc, urlDest, apMon, conf, logger)
            else:
                if len(self.transfers) == 0:
                    raise FDTCopyException("No transfer requests found in '%s'" % fileName)
        except IndexError, ex:
            lineNum = index + 1 # count lines from 1 for user-friendly error message
            m = "Can't parse file '%s', line: '%s' line number: %s, reason: %s" % (fileName, line, lineNum, ex)
            raise FDTCopyException(m)

        
    

class ConfigFDTCopy(Config):
    """Class holding various options and settings which are either predefined
       in the configuration file, overriding from command line options is
       considered.
    """
    
    # mandatory configuration options, have to be integers
    _mandatoryInt = ["timeout"]
    # mandatory configuration options, have to be strings
    _mandatoryStr = ["debug"]
    
    
    def __init__(self, args):
        usage = \
"""usage: %prog [options] source_url destination_url
  %prog [options] --copyjobfile=filelist
  %prog fdt://host1:port/path1/to/file1 fdt://host2:port/path2/to/file2
""" 
        # 1 - shall point to the same directory
        currDir = os.path.abspath(__file__).rsplit(os.sep, 1)[0]
        currDirConfig = os.path.join(currDir, "fdtcp.conf")
        # consider only config file being in the same directory
        # as well as in the system config directory location
        locations = [currDirConfig, "/etc/fdtcp/fdtcp.conf"]           
        Config.__init__(self, args, locations, usage = usage)
        
        
        
    def _processUserProxy(self):
        """Get user X509 grid certificate proxy. Proxy info is taken
           1) command line argument -x, --x509userproxy (optional command line argument)
           2) X509_USER_PROXY env. variable
           3) /tmp/x509up_u<uid>
        """
        optName = "x509userproxy"
        if self.get(optName) != None:
            # already set
            return
        elif os.environ.get("X509_USER_PROXY", None) != None:
            setattr(self._options, optName, os.environ.get("X509_USER_PROXY"))
        else:
            path = "/tmp/x509up_u%s" % os.getuid()
            setattr(self._options, optName, path)
                
    
    
    def _defineCommandLineOptions(self):
        help = "the path to the report file"
        self._parser.add_option("-r", "--report", help = help)
        help = "debug output level, for possible values see the config file"
        self._parser.add_option("-d", "--debug", help = help)
        help = "file containing source_url destination_url pairs"
        self._parser.add_option("-f", "--copyjobfile", help = help)
        help = "print this help"
        self._parser.add_option("-h", "--help", help = help, action = 'help')
        help = "configuration file for fdcp"
        self._parser.add_option("-c", "--config", help = help)
        help = "timeout in seconds for initial connectivity tests"
        self._parser.add_option("-t", "--timeout", help = help)
        help = "optional argument - output log file"
        self._parser.add_option("-l", "--logFile", help = help)
        help = "user's proxy certificate file from custom location"
        self._parser.add_option("-x", "--x509userproxy", help = help)
        


    def processCommandLineOptions(self, args):
        """This method gets called from base class"""
        self._defineCommandLineOptions()

        # opts - new processed options, items defined above appear as attributes
        # args - remainder of the input array
        opts, args = self._parser.parse_args(args = args)
        
        if not opts.copyjobfile:
            try:
                opts.urlSrc = args[-2]
                opts.urlDest = args[-1]
            except IndexError, ex:
                self._parser.print_help()
                m = "Incorrect command line options, see --help" 
                raise ConfigurationException(m)
        else:
            if len(args) > 0:
                m = ("Incorrect command line options, can't take further "
                     "arguments along with --copyjobfile, see --help ")
                raise ConfigurationException(m)
            
        self._options = opts
        self._processUserProxy()
        
                


# TODO
# encapsulate into FDTCopyApplication class



# TODO
# if having multi-threaded, concurrent processing of the transfers,
#    exclusive access to the file has be assured
def generateReport(transfer, fileName):
    # don't check any exceptions, will just be propagated to the caller
    t = transfer
    f = open(fileName, "w+")
    for trf in t.files:
        o = ("%s:%s %s:%s %s %s\n" % (t.hostSrc, trf.fileSrc, t.hostDest,
                                      trf.fileDest, t.result, t.log))
        f.write(o)
        f.flush()
    f.close()
        
                            
        
def main():
    # all values and action information held in the conf object
    optBackup = sys.argv[:]
    try:
        conf = ConfigFDTCopy(sys.argv[1:])
        conf.sanitize()
    except ConfigurationException, ex:
        print "fdtcp failed to start, reason: %s" % ex
        sys.exit(1)
    
    logger = Logger(name = "fdtcp", logFile = conf.get("logFile"),
                    level = conf.get("debug"))
    logger.info("fdtcp starting ...")
    logger.debug("Search sys.path: '%s'" % sys.path)
    logger.debug("PYRO_STORAGE: '%s'" % os.environ.get("PYRO_STORAGE"))

    logger.debug("Input command line arguments: '%s'" % optBackup)
    logger.debug("Configuration values (processed): '%s'" % conf._options)
    
    apMonDestConf = conf.get("apMonDestinations")
    apMonDestinations = tuple(apMonDestConf.split(','))
    logger.info("Initializing MonALISA ApMon, destinations: %s ..." % (apMonDestinations,))
    apMon = apmon.ApMon(apMonDestinations)
    apMon.enableBgMonitoring(True)
    
    # use DNS names rather than IP address
    Pyro.config.PYRO_DNS_URI = True
    

    try:
        transfers = Transfers(conf, apMon, logger)
    except FDTCopyException, ex:
        logger.fatal(ex)
        # everything failed - all transfers
        if conf.get("report"):
            # TODO
            # if necessary - may take just the input or the copyjobfile
            # and generate something with everything assigned 1
            logger.error("Report file was required, can't generate it with "
                         "failure on this stage ...")
        apMon.free()
        logger.warn("fdtcp finished.")
        logger.close()
        sys.exit(1)
    
    
    try:
        for transfer in transfers.transfers.values():
            try:
                transfer.performTransfer()
                transfer.result = 0
            except FDTCopyException, ex:
                logger.error("Transfer failed, reason: %s" % ex)
                transfer.result = 1
                transfer.log = ex
            except ExecutorException, ex:
                # TODO
                # this type of exception is too low-level, should be wrapper on an upper level
                logger.error("Transfer failed, reason: %s" % ex)
                transfer.result = 1
                transfer.log = ex                
            except FDTDException, ex:
                # TODO
                # this may (most likely) only be raised from authChain from Executor
                # which needs to be made more general and not bound (e.g. by exception
                # types it raises) to fdtd
                logger.error("Transfer failed, reason: %s" % ex)
                transfer.result = 1
                transfer.log = ex                
            except Exception, ex:
                m = "Exception was caught ('%s'), reason: %s" % (ex.__class__.__name__, ex)
                logger.error(m)
                # will also print remote PYRO traceback
                logger.fatal(''.join(Pyro.util.getPyroTraceback(ex)))
                transfer.result = 1
                transfer.log = m
            except KeyboardInterrupt:
                m = "Interrupted from keyboard."
                logger.fatal(m + " Please wait  ...")
                transfer.result = 1
                transfer.log = m
                break
    finally:
        for transfer in transfers.transfers.values():
            try:
                try:
                    transfer.performCleanup()
                except Exception, ex:
                    logger.error("Exception during cleanup, reason: %s" % ex)
                # check if the actual FDTCopy attribute exists - might have failed while creating it
                if transfer.sender:
                    transfer.sender.proxy._release()
                if transfer.receiver:
                    transfer.receiver.proxy._release()
            finally:
                fileName = conf.get("report")
                if fileName:
                    logger.info("Going to generate report file '%s' for this transfer ..." % fileName)
                    generateReport(transfer, fileName)
        apMon.free()
        logger.warn("fdtcp finished.")
        logger.close()
            

    
        
if __name__ == "__main__":
    main()
